﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading;
using System.Text;
using System.Threading.Tasks;

namespace NTL.Assignment
{
    /// <summary>
    /// File reader employes reader threads (consumers) to search for content of fileNames.
    /// File reader finds pairs of duplicate fileNames in terms of file content.
    /// File reader useses the blocking queue (constructer parameter) to place produced 
    /// items (duplicate file name pairs).
    /// </summary>
    public class FileReader
    {
        /// <summary>
        /// Producer consumer queue.
        /// Producer : file reader thread.
        /// Consumer : duplicate writer thread.
        /// Elements : fileNames having duplicate content.
        /// </summary>
        private ConcurrentQueue<IList<string>> blockingQueue;

        /// <summary>
        /// Reset event to synchronize pause\resume.
        /// </summary>
        private ManualResetEvent pauseResumeEvent = new ManualResetEvent(true);

        /// <summary>
        /// Reset event to synchronize start\abort\restart.
        /// </summary>
        private ManualResetEvent abortEvent = new ManualResetEvent(false);

        /// <summary>
        /// List of file-name to file-content mappings.
        /// </summary>
        private IList<KeyValuePair<string, string>> nameContentMap;

        /// <summary>
        /// List of reader threads.
        /// </summary>
        private IList<Thread> readerThreads = new List<Thread>();

        /// <summary>
        /// Queue of file names.
        /// </summary>
        private ConcurrentQueue<string> fileNames;

        /// <summary>
        /// Initilizes a new instance of the <see cref="FileReader"/> class.
        /// </summary>
        /// <param name="blockingQueue">Blocking queue to place the discovered matches.</param>
        /// <param name="searchDirectoryPath">Path to the root directory to search for fileNames.</param>
        /// <param name="readerThreadCount">Number of searcher threads to use.</param>
        public FileReader(ConcurrentQueue<IList<string>> blockingQueue, string searchDirectoryPath, int threadCount)
        {
            this.blockingQueue = blockingQueue;
            this.SearchDirectoryPath = searchDirectoryPath;
            this.ReaderThreadCount = threadCount;
        }

        /// <summary>
        /// Gets or sets the search directory.
        /// </summary>
        public string SearchDirectoryPath { get; set; }

        /// <summary>
        /// Gets or sets the number of reader threads to use.
        /// </summary>
        public int ReaderThreadCount { get; set; }

        /// <summary>
        /// The number of files processed.
        /// </summary>
        public int Count { get; set; }

        /// <summary>
        /// Trigger reader threads. This is an async call.
        /// </summary>
        public void Read()
        {
            DirectoryInfo searchDirectory = new DirectoryInfo(SearchDirectoryPath);
            FileInfo[] fileInfos = searchDirectory.GetFiles("*.*", SearchOption.AllDirectories);

            Count = 0;

            fileNames = new ConcurrentQueue<string>();
            nameContentMap = new List<KeyValuePair<string, string>>();

            foreach (FileInfo fileInfo in fileInfos)
            {
                fileNames.Enqueue(fileInfo.FullName);
            }

            // Trigger content reader threads
            for (int i = 0; i < ReaderThreadCount; i++)
            {
                Thread t = new Thread(MatchContentInParallel);
                readerThreads.Add(t);
                t.Start();
            }
        }

        /// <summary>
        /// Pause the reader threads.
        /// </summary>
        public void Pause()
        {
            // Setting the state to non-signaled to block the reader threads.
            pauseResumeEvent.Reset();
        }

        /// <summary>
        /// Resume reader threads.
        /// </summary>
        public void Resume()
        {
            // Resuming reader threads.
            pauseResumeEvent.Set();
        }

        /// <summary>
        /// Aborts searcher threads. This is a synchronous call.
        /// Abort will also drop existing items in the queue.
        /// </summary>
        public void Abort()
        {
            pauseResumeEvent.Set();
            abortEvent.Set();

            foreach (Thread readerThread in readerThreads)
            {
                readerThread.Join(TimeSpan.FromMilliseconds(500));
            }

            // Empty the queue, since we are aborting.

            while (!blockingQueue.IsEmpty)
            {
                IList<string> element = null;
                blockingQueue.TryDequeue(out element);
            }     
        }

        /// <summary>
        /// Restarts reader threads.
        /// </summary>
        public void Restart()
        {
            readerThreads.Clear();

            // Reset abort event
            abortEvent.Reset();

            // Read again
            Read();
        }

        /// <summary>
        /// Search for fileNames with duplicate content in parallel (delegete for reader threads).
        /// </summary>
        private void MatchContentInParallel()
        {
            while (true)
            {
                // Will wait here when pause is triggered.
                pauseResumeEvent.WaitOne();

                string fileName;
                fileNames.TryDequeue(out fileName);

                if (fileName != null)
                {
                    using (StreamReader reader = new StreamReader(fileName))
                    {
                        string content = reader.ReadToEnd();

                        KeyValuePair<string, string> pair = new KeyValuePair<string, string>(fileName, content);

                        lock (nameContentMap)
                        {
                            nameContentMap.Add(pair);

                            // Increase the number of files processed so far.
                            Count++;

                            // Comparing the last inserted with (size - 1) existing items.
                            for (int i = 0; i < (nameContentMap.Count - 1); i++)
                            {
                                string existingFilePath = nameContentMap[i].Key;
                                string existingFileContent = nameContentMap[i].Value;

                                // Check if matching content exists.
                                if (pair.Value.Equals(existingFileContent))
                                {
                                    IList<string> dup = new List<string>(2);

                                    // Add matching pair
                                    dup.Add(existingFilePath);
                                    dup.Add(pair.Key);

                                    blockingQueue.Enqueue(dup);
                                }
                            }
                        }
                    }
                }
                else
                {
                    // All files under the search directory have been read.
                    break;
                }

                // Break the loop if abort is triggered
                if (abortEvent.WaitOne(0))
                {
                    break;
                }
            }
        }
    }
}
